<?php
/**
 * Copyright (c) 2020
 * 摘    要：
 * 作    者：san
 * 修改日期：2020.04.03
 */

namespace App\Service;

use App\Library\Clog\Log;
use App\Library\AutoDs\Ansible;
use App\Library\AutoDs\Folder;
use App\Library\AutoDs\Repo;
use App\Library\AutoDs\Task;
use App\Model\Environment;
use App\Model\Project;
use App\Model\Role;
use App\Model\User;
use App\Model\WorkSpaceUser;
use ErrorException;
use Exception;
use Hyperf\Database\Model\Builder;
use Hyperf\Database\Model\Collection;
use Hyperf\Database\Model\Model;
use Hyperf\Utils\Context;
use Hyperf\Utils\Coroutine;
use Hyperf\Utils\Exception\ParallelExecutionException;
use Hyperf\Utils\Parallel;

class ProjectService extends BaseService
{
    const  TAG = 'ProjectService';

    /**
     * ProjectService constructor.
     */
    public function __construct()
    {
        parent::__construct();

        $this->redis = redis();
    }

    /**
     * 获取项目列表
     *
     * @param int $userId
     * @param int $page
     * @param int $pageSize
     * @param array $condition
     * @return array
     */
    public function query(int $userId, int $page, int $pageSize, array $condition)
    {
        //查询此用户所在的空间
        $tmp          = [];
        $workspaceIds = WorkSpaceUser::query()->where('user_id', $userId)->get('workspace_id');
        foreach ($workspaceIds as $workspaceId) {
            array_push($tmp, $workspaceId['workspace_id']);
        }

        $fields = [
            'project.id',
            'project.name',
            'project.level',
            'project.repo_mode',
            'project.audit',
            'project.status',
            'environment.name as env_name',
            'workspace.name as workspace_name',
        ];

        $query = Project::query()
            ->select($fields)
            ->leftJoin('environment', 'environment.id', '=', 'project.level')
            ->leftJoin('workspace', 'workspace.id', '=', 'project.workspace_id')
            ->whereNull('environment.deleted_at')
            ->whereNull('workspace.deleted_at')
            ->whereIn('project.workspace_id', $tmp);

        if ($condition) {
            $query = $query->where($condition);
        }

        $count  = $query->count();
        $result = $query->limit($pageSize)->offset(($page - 1) * $pageSize)
            ->orderBy('project.id', 'desc')
            ->get();

        return [
            'list'  => $result,
            'total' => $count,
        ];
    }

    /**
     * @throws ErrorException
     * @return array
     */
    public function all()
    {
        $uuid   = Context::get('user')->user_id;
        $fields = [
            'project.id',
            'project.name',
            'project.level',
            'environment.name as env_name',
        ];

        if ($uuid != 1) {
            $workspaceIds = WorkSpaceUser::query(true)
                ->select('workspace_id')
                ->where('user_id', $uuid)
                ->whereIn('role_id', [Role::ROLE_OWNER, Role::ROLE_MASTER])
                ->get();
            if (!$workspaceIds) {
                throw new ErrorException(t('message.12027'));
            }
            $workspaceIds = $workspaceIds->toArray();
        } else {
            $workspaceIds = [];
        }

        $query = Project::query(true)
            ->select($fields)
            ->leftJoin('environment', 'environment.id', '=', 'project.level')
            ->whereNull('environment.deleted_at');

        if ($workspaceIds) {
            $query = $query->whereIn('project.workspace_id', $workspaceIds);
        }

        return $query->orderBy('project.id', 'desc')->get();
    }

    /**
     * @param $data
     * @throws ErrorException
     * @return bool
     */
    public function add($data)
    {
        $data['user_id'] = Context::get('user')->user_id;;
        $res = Project::query()->insert($data);
        if (!$res) {
            throw new ErrorException(t('message.12002'));
        }
        return true;
    }

    /**
     * @param $id
     * @throws ErrorException
     * @return bool
     */
    public function copy($id)
    {
        $detail = $this->_checkExits($id);

        $data = $detail->toArray();
        unset($data['id']);
        $data['name'] = $data['name'] . '_Copy';
        $res          = Project::query()->insert($data);
        if (!$res) {
            throw new ErrorException(t('message.12002'));
        }
        return true;
    }

    /**
     * @param $data
     * @throws ErrorException
     * @return bool
     */
    public function edit($data)
    {
        $detail = $this->_checkExits($data['id']);

        if ($data['aliyun_sync'] == 0) {
            $data['aliyun_id']          = '';
            $data['aliyun_region']      = '';
            $data['aliyun_listen_port'] = '';
        } else {
            $data['hosts'] = '';
        }

        if ($data['is_open_gray'] == 0) {
            $data['gray_hosts'] = '';
        }

        unset($data['id'], $data['user_id'], $data['version']);

        $res = Project::query(true)->where('id', $detail->id)->update($data);
        if (!$res) {
            throw new ErrorException(t('message.12002'));
        }

        $this->_saveAnsibleHosts(Project::query(true)->find($detail->id));

        return true;
    }

    /**
     * @param $id
     * @throws ErrorException
     * @return array
     */
    public function show($id)
    {
        $detail = $this->_checkExits($id);

        $detail->keep_version_num   = (string)$detail->keep_version_num;
        $detail->aliyun_listen_port = (int)$detail->aliyun_listen_port;

        return $detail->toArray();
    }

    /**
     * @param $id
     * @throws ErrorException
     * @throws Exception
     * @return bool
     */
    public function delete($id)
    {
        $detail = $this->_checkExits($id);

        $res = $detail->delete();
        if (!$res) {
            throw new ErrorException(t('message.12002'));
        }

        return true;
    }

    /**
     * 获取阿里云SLB端口号
     *
     * @param $aliyunId
     * @param $aliyunRegion
     * @throws Exception
     * @return array
     */
    public function aliyunPort($aliyunId, $aliyunRegion)
    {
        $result = AliyunEcs::requestApi('DescribeLoadBalancerAttribute', $aliyunId, $aliyunRegion);

        if (isset($result['ListenerPorts']) && count($result['ListenerPorts']) > 0) {
            return $result['ListenerPorts']['ListenerPort'];
        }

        throw new ErrorException(t('message.12002'));
    }

    /**
     * 项目检测
     *
     * @param $projectId
     * @return array
     */
    public function detection($projectId)
    {
        $parallel = new Parallel();
        $project  = Project::getInstance($projectId);

        // 1、检测宿主机检出目录是否可读写
        $parallel->add(function () use ($projectId) {
            $data          = $this->_checkDirectoryIsReadable($projectId);
            $data['id']    = Coroutine::id();
            $data['title'] = '【宿主机】检测检出目录是否可读写';
            return $data;
        });

        // 2.检测宿主机ssh是否加入git信任
        $parallel->add(function () use ($projectId) {
            $data          = $this->_checkSshJoinGitTrust($projectId);
            $data['id']    = Coroutine::id();
            $data['title'] = '【宿主机】检测ssh是否加入git信任';
            return $data;
        });

        // 3.检测 ansible 是否安装
        if ($project->ansible) {
            $parallel->add(function () use ($projectId) {
                $data          = $this->_checkInstallationAnsible($projectId);
                $data['id']    = Coroutine::id();
                $data['title'] = '【宿主机】检测ansible是否安装';
                return $data;
            });
        }

        // 4.检测php用户是否加入目标机ssh信任
        $parallel->add(function () use ($projectId) {
            $data          = $this->_checkWhetherThePhpSshTrust($projectId);
            $data['id']    = Coroutine::id();
            $data['title'] = '【目标机】检测php用户是否加入ssh信任';
            return $data;
        });

        // 5.检测 ansible 连接目标机是否正常
        if ($project->ansible) {
            $parallel->add(function () use ($projectId) {
                $data          = $this->_checkTargetAnsible($projectId);
                $data['id']    = Coroutine::id();
                $data['title'] = '【目标机】检测ansible连接是否正常';
                return $data;
            });
        }

        // 6.检测php用户是否具有目标机release目录读写权限
        $parallel->add(function () use ($projectId) {
            $data          = $this->_checkPHPHasPermissionsRorTheReleaseDirectory($projectId);
            $data['id']    = Coroutine::id();
            $data['title'] = '【目标机】检测php用户是否具有release目录读写权限';
            return $data;
        });

        // 7.路径必须为绝对路径
        $parallel->add(function () use ($projectId) {
            $data          = $this->_checkIsAbsolutePath($projectId);
            $data['id']    = Coroutine::id();
            $data['title'] = '【项目】检测路径是否为绝对路径';
            return $data;
        });

        try {
            $result = array_values($parallel->wait());
            usort($result, array($this, '_compare'));
            return $result;
        } catch (ParallelExecutionException $e) {
            Log::info(self::TAG, 'detection-error', ['result' => $e->getResults(), 'error' => $e->getThrowables()]);
        }
    }

    public function _compare($a, $b)
    {
        if ($a['id'] == $b['id']) {
            return 0;
        }
        return ($a['id'] > $b['id']) ? 1 : -1;
    }

    /**
     * 检测宿主机检出目录是否可读写
     *
     * @param $projectId
     * @return array
     */
    private function _checkDirectoryIsReadable($projectId)
    {
        $status = true;
        $error  = '';
        try {
            $project     = Project::getInstance($projectId);
            $codeBaseDir = Project::getDeployFromDir();

            $isWritable = is_dir($codeBaseDir) ? is_writable($codeBaseDir) : @mkdir($codeBaseDir, 0755, true);
            if (!$isWritable) {
                $error = t('message.12010', [getenv("USER"), $project->deploy_from]);
                throw new ErrorException($error);
            }
        } catch (Exception $exception) {
            $status = false;
            $error  = $exception->getMessage();
        }

        return [
            'status' => $status,
            'error'  => $error,
        ];
    }

    /**
     * 检测宿主机ssh是否加入git信任
     *
     * @param $projectId
     * @return array
     */
    private function _checkSshJoinGitTrust($projectId)
    {
        $status = true;
        $error  = '';
        try {
            $project = Project::getInstance($projectId);
            $repo    = Repo::getInstance($project);

            $ret = $repo->updateRepo();
            if (!$ret) {
                $message = $project->repo_type == Project::REPO_GIT ?
                    t('message.12008', [getenv("USER")]) : t('message.12009');
                $error   = t('message.12011', [$message]);
                throw new ErrorException($error);
            }
        } catch (Exception $exception) {
            $status = false;
            $error  = $exception->getMessage();
        }

        return [
            'status' => $status,
            'error'  => $error,
        ];
    }

    /**
     * 检测 ansible 是否安装
     *
     * @param $projectId
     * @return array
     */
    private function _checkInstallationAnsible($projectId)
    {
        $status = true;
        $error  = '';
        try {
            $project = Project::getInstance($projectId);
            $ansible = new Ansible($project);
            $ret     = $ansible->test();
            if (!$ret) {
                throw new ErrorException(t('message.12014'));
            }
        } catch (Exception $exception) {
            $status = false;
            $error  = $exception->getMessage();
        }

        return [
            'status' => $status,
            'error'  => $error,
        ];
    }

    /**
     * 检测php用户是否加入目标机ssh信任
     *
     * @param $projectId
     * @return array
     */
    private function _checkWhetherThePhpSshTrust($projectId)
    {
        $status = true;
        $error  = '';
        try {
            $project    = Project::getInstance($projectId);
            $autodsTask = new Task($project);
            $command    = 'id';
            $ret        = $autodsTask->runRemoteTaskCommandPackage([$command]);
            if (!$ret) {
                $error = t('message.12015', [
                    getenv("USER"),
                    $project->release_user,
                    $project->release_to
                ]);
                throw new ErrorException($error);
            }
        } catch (Exception $exception) {
            $status = false;
            $error  = $exception->getMessage();
        }
        return [
            'status' => $status,
            'error'  => $error,
        ];
    }

    /**
     * 检测 ansible 连接目标机是否正常
     *
     * @param $projectId
     * @return array
     */
    private function _checkTargetAnsible($projectId)
    {
        $status = true;
        $error  = '';
        try {
            $project = Project::getInstance($projectId);
            $ansible = new Ansible($project);
            $ret     = $ansible->ping();
            if (!$ret) {
                $error = t('message.12018');
                throw new ErrorException($error);
            }
        } catch (Exception $exception) {
            $status = false;
            $error  = $exception->getMessage();
        }

        return [
            'status' => $status,
            'error'  => $error,
        ];
    }

    /**
     * 检测php用户是否具有目标机release目录读写权限
     *
     * @param $projectId
     * @return array
     */
    private function _checkPHPHasPermissionsRorTheReleaseDirectory($projectId)
    {
        $status = true;
        $error  = '';
        try {
            $project    = Project::getInstance($projectId);
            $autodsTask = new Task($project);

            $tmpDir  = 'detection' . time();
            $command = sprintf('mkdir -p %s', Project::getReleaseVersionDir($tmpDir));

            $ret = $autodsTask->runRemoteTaskCommandPackage([$command]);
            if (!$ret) {
                $error = t('message.12016', [
                    $project->release_user,
                    $project->release_library,
                ]);
                throw new ErrorException($error);
            }
            // 清除
            $command = sprintf('rm -rf %s', Project::getReleaseVersionDir($tmpDir));
            $autodsTask->runRemoteTaskCommandPackage([$command]);
        } catch (Exception $exception) {
            $status = false;
            $error  = $exception->getMessage();
        }

        return [
            'status' => $status,
            'error'  => $error,
        ];
    }

    /**
     * 路径必须为绝对路径
     *
     * @param $projectId
     * @return array
     */
    private function _checkIsAbsolutePath($projectId)
    {
        $status = true;
        $error  = '';
        try {
            $project = Project::getInstance($projectId);

            $needAbsoluteDir = [
                t('message.12066') => $project->deploy_from,
                t('message.12067') => $project->release_to,
                t('message.12068') => $project->release_library,
            ];
            foreach ($needAbsoluteDir as $tips => $dir) {
                if (0 !== strpos($dir, '/')) {
                    $$error = t('message.12013', [
                        sprintf('%s:%s', $tips, $dir),
                    ]);
                    throw new ErrorException($error);
                }
            }
        } catch (Exception $exception) {
            $status = false;
            $error  = $exception->getMessage();
        }

        return [
            'status' => $status,
            'error'  => $error,
        ];
    }

    /**
     * @param $id
     * @throws ErrorException
     * @return Builder|Builder[]|Collection|Model|null
     */
    private function _checkExits($id)
    {
        $detail = Project::query(true)->find($id);
        if (!$detail) throw new ErrorException("记录不存在");

        return $detail;
    }

    /**
     * 刷新host
     *
     * @param Project $project
     * @throws ErrorException
     * @throws Exception
     * @return bool
     */
    public static function _saveAnsibleHosts(Project $project)
    {
        $filePath = Project::getAnsibleHostsFile($project->id);
        //刷新阿里云ECS Host
        if ($project->aliyun_sync == 1) {
            $result = AliyunEcs::requestApi('DescribeHealthStatus', $project->aliyun_id, $project->aliyun_region, $project->aliyun_listen_port);
            $tmp    = [];
            if (isset($result['BackendServers']) && count($result['BackendServers']) > 0) {
                foreach ($result['BackendServers']['BackendServer'] as $server) {
                    $tmp[] = $server['ServerIp'];
                }
                $host = implode(PHP_EOL, $tmp);
                //保存host
                $project->hosts = $host;
                $project->save();
            } else {
                throw new ErrorException(t('message.12019', [$filePath]));
            }
        }

        $host = $project->hosts;
        $ret  = @file_put_contents($filePath, $host);

        if (!$ret) {
            throw new ErrorException(t('message.12019', [$filePath]));
        }

        //是否开启了灰度
        if ($project->is_open_gray) {
            $filePath = Project::getAnsibleGrayHostsFile($project->id);
            $ret      = @file_put_contents($filePath, $project->gray_hosts);
            if (!$ret) {
                throw new ErrorException(t('message.12019', [$filePath]));
            }
        }

        return true;
    }

    /**
     * 上线单 分类
     *
     * @throws ErrorException
     * @return  array
     */
    public function classification()
    {
        $uuid = Context::get('user')->user_id;
        //非管理员筛选 当前 账号所属部门下的项目，该ID角色必须是 部门所属人或者项目负责人
        if ($uuid != User::ADMIN_UID) {
            $workspaceIds = WorkSpaceUser::query(true)
                ->select('workspace_id')
                ->where('user_id', $uuid)
                ->get();
            if (!$workspaceIds) {
                throw new ErrorException(t('message.12027'));
            }
            $workspaceIds = $workspaceIds->toArray();
        } else {
            $workspaceIds = [];
        }

        $query = Project::query(true)
            ->select(['project.*', 'e.name as env_name'])
            ->leftJoin('environment as e', 'project.level', 'e.id');
        if ($workspaceIds) {
            $query = $query->whereIn('project.workspace_id', $workspaceIds);
        }

        $result = $query->get()->toArray();
        $tmp    = [];

        if ($result) {
            //环境
            foreach ($result as $value) {
                $tmp[Environment::getAliasMap($value['level'])][] = $value;
            }
        }

        return $tmp;
    }

    /**
     * 线上文件指纹校验
     *
     * @param $projectId
     * @param $file
     * @throws ErrorException
     * @return array
     */
    public function md5($projectId, $file)
    {
        $project    = $this->_checkExits($projectId);
        $instance   = Project::getInstance($projectId);
        $folder     = new Folder($instance);
        $projectDir = $project->release_to;
        $file       = sprintf("%s/%s", rtrim($projectDir, '/'), $file);

        $folder->getFileMd5($file);
        $logs = $folder->getExeLog();

        return nl2br($logs);
    }
}
